package com.nulldev.util.web.YouTubeExtractor.extractors.third_party;

import java.net.NoRouteToHostException;
import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.codecs.uniapi.UniBase64;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Date.Dates;
import com.nulldev.util.io.FastIO.FastIO;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.YoutubeExtractionOptions;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.formats.AudioCodec;
import com.nulldev.util.web.YouTubeExtractor.formats.FileType;
import com.nulldev.util.web.YouTubeExtractor.formats.Format;
import com.nulldev.util.web.YouTubeExtractor.formats.Formats;
import com.nulldev.util.web.YouTubeExtractor.formats.VideoCodec;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist;
import com.nulldev.util.web.YouTubeExtractor.signatures.extractor.Extractor;
import com.nulldev.util.web.YouTubeExtractor.subtitles.YouTubeCaptions;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractedVideo;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractionResult;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo.FormatType;
import com.nulldev.util.web.legacyURI.LegacyURIUtils;

public class UnAgeRestrictorExtractor extends IYouTubeExtractor {

	private static final IExecutorManager td = ExecutorManager.get();
	private static final Logger logger = LoggerFactory.getLoggerD(true, true);
	private static final String[] DESC_IN =
		{ "\r\n", "\n", "%3B" };
	private static final String[] DESC_OUT =
		{ "<br>", "<br>", ";" };

	private static final String VIDEO_PROXY = "https://phx.4everproxy.com";

	private static String generateURL(final String videoID, final Map<String, Object> arguments) {
		final StringBuilder b = new StringBuilder("https://youtube-proxy.zerody.one/getPlayer?videoId=" + videoID + "&clientName=WEB&clientVersion="
				+ YouTubeExtractorConstants.CLIENT_VERSION + "&isEmbed=false" + "&signatureTimestamp=" + YouTubeExtractorConstants.SIGNATURE_TIMESTAMP
				+ "&reason=LOGIN_REQUIRED" + "&startTimeSecs=0&client=nTube");
		if (arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS)) {
			b.append("&bpctr=");
			b.append(System.currentTimeMillis());
		}
		return b.toString();
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(videoID, null, options);
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, String extraArgs, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(IYouTubeExtractorConstants.convertFromLegacy(videoID, extraArgs, options));
	}

	private class ExtractedRequest {
		public final String result;

		public ExtractedRequest(final String result) {
			this.result = result;
		}
	}

	@Override
	public Executable<ExtractedVideo> generateTask(final Map<String, Object> arguments) {
		if (!isValidMap(arguments))
			throw new IllegalArgumentException("Invalid map: " + arguments);
		final String videoID = (String) arguments.get(MAP_TYPE_VIDEO_ID);
		if (!isValidVideoID(videoID))
			throw new IllegalArgumentException("Invalid video ID: " + videoID);
		return td.makeTask(() -> {
			final List<YouTubeVideo> results = FastLists.list(32);
			final Object _result = td.submit(() -> {
				logger.debug("=> Generating URL...");
				final URL url = URLUtils.parseURL(generateURL(videoID, arguments));
				logger.debug("URL: " + url);
				Entry[] _args = YouTubeExtractor.DEFAULT_COOKIES;
				try {
					logger.debug("=> Downloading metadata...");
					final HttpResponse<String> x = HttpClient.getString().url(url).timeout(Duration.ofSeconds(20)).headers(_args)
							.header(new Entry("Referrer", "https://www.youtube.com/watch?v=" + videoID))
							.header(new Entry("X-SPF-Previous", "https://www.youtube.com/watch?v=" + videoID))
							.header(new Entry("X-SPF-Referer", "https://www.youtube.com/watch?v=" + videoID)).header("From", "nTube 1.5.0, UARE")
							.create(HttpMethod.GET);
					return new ExtractedRequest(StringsUtil.replace(x.dataThenClose(), "\u0008", ""));
				} catch (Throwable e) {
					if (e instanceof NoRouteToHostException) {
						return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
								.setYTMessage("No route to youtube.com was found, this might be the internet.");
					}
					return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
							.setYTMessage("Hit the following exception: " + e.getMessage());
				}
			}).waitTillCompletion();
			if (_result instanceof ExtractedVideo) {
				return ((ExtractedVideo) _result);
			}
			final String result = ((ExtractedRequest) _result).result;
			final UniJsonValue player_response = UniJsonValue.parse(result);
			if (arguments.containsKey(MAP_TYPE_ENABLE_DEBUG_API_RESPONSE_PRINT))
				logger.info(player_response);
			final UniJsonValue videoDetails = player_response.get("videoDetails");
			logger.debug("Accquiring decipherer.");
			final Executable<Extractor> e = ExecutorManager.globalAsyncSubmit(() -> {
				try {
					return YouTubeExtractor.extractor.getAuto();
				} catch (Exception ef) {
					throw new RuntimeException(ef);
				}
			});
			logger.debug("Checking if proxying is requried.");
			final variable<Boolean> useProxying = new variable<Boolean>(false);
			if (isEnabled(arguments, MAP_TYPE_USE_BACKEND_PROXY)) {
				logger.debug("Enabling proxy on request.");
				useProxying.set(true);
			}
			if (player_response.contains("proxy") && player_response.get("proxy").bool("youtubeGcrFlagSet", false)) {
				logger.debug("Enabling proxy as it is required.");
				useProxying.set(true);
			}
			final List<YouTubeCaptions> captions = FastLists.list();
			if (player_response.contains("captions")) {
				// captions found
				final UniJsonValue ct = player_response.getSafe("captions").getSafe("playerCaptionsTracklistRenderer").getIgnoreNull("captionTracks");
				if (ct == null) {
					logger.error("Failed to load captions, captionTracks was null."); // probably no subtitles or contributions are only enabled.
				} else {
					final UniJsonIterableValue captionArray = ct.asArray();
					if (captionArray != null && !captionArray.isNull()) {
						for (final UniJsonValue _v : captionArray) {
							final UniJsonValue caption = _v;
							final String captionURL = caption.getString("baseUrl");
							final String captionText = StringsUtil._capitalizeString(caption.get("name").getString("simpleText"));
							final String captionCode = caption.getString("languageCode");
							captions.add(new YouTubeCaptions(captionURL, captionText, captionCode));
						}
					}
				}
			}
			Playlist pl = null;
			UniJsonValue keywords = UniJsonValue.array();
			try {
				keywords = videoDetails.get("keywords").asArray();
			} catch (Exception _e) {
			}
			if (keywords == null) {
				keywords = UniJsonValue.array();
			}
			final YouTubeMetadata metadata = new YouTubeMetadata(videoID, videoDetails.getString("title", "Unknown title."),
					videoDetails.getString("author", "Unknown author."), videoDetails.getString("channelId", ""), videoDetails.getLong("viewCount", -1),
					videoDetails.getLong("lengthSeconds", -1), videoDetails.getDouble("averageRating", 5), -1, -1, false, keywords.toArray(), captions);
			metadata.setMap(result);
			metadata.setDescription(StringsUtil.replaceEach(videoDetails.getString("shortDescription", "A description wasn't found."), DESC_IN, DESC_OUT));
			if (videoDetails.hasValue("isUpcoming") && videoDetails.getBoolean("isUpcoming")) {
				logger.error("Failed -> This video isn't live yet!");
				final long time = player_response.get("playabilityStatus").get("liveStreamability").get("liveStreamabilityRenderer").get("offlineSlate")
						.get("liveStreamOfflineSlateRenderer").getLong("scheduledStartTime", -1);
				final String times = TimeUtil.formatSecondsL(time - System.currentTimeMillis() / 1000);
				return new ExtractedVideo(results, metadata, ExtractionResult.FAILED).setMessage("ERROR_UPCOMING_STREAM").setYTMessage(
						"This video/stream will be live in " + times + " from now. (Stream Start: " + Dates.time((long) (time * 1000)).toString() + ")");
			}
			logger.debug("Processing legacy map.");
			final UniJsonValue streamingData = player_response.getSafe("streamingData");
			final UniJsonIterableValue formats = streamingData.get("formats").asArray();
			if (!formats.isNull()) {
				for (final UniJsonValue _v : formats) {
					final UniJsonValue v = _v;
					String vurl = v.getString("url", null);
					if (v.contains("cipher", "signatureCipher")) {
						@SuppressWarnings("deprecation")
						final Map<String, String> y = LegacyURIUtils
								.queryStringToMap(v.getStrings("signatureCipher", "cipher").replaceAll(Pattern.quote("\\u0026"), "&"));
						vurl = y.get("url");
						vurl += "&" + y.get("sp") + "=" + e.waitTillCompletion().decrypt(y.get("s"));
					}
					final int itag = v.getInt("itag");
					if (vurl.contains("&dur=0.000")) {
						logger.warn("A source had zero duration, skipped.");
						continue;
					}
					if (useProxying.get()) {
						vurl = VIDEO_PROXY + "/direct/" + UniBase64.encodeFromString(vurl);
					}
					results.add(new YouTubeVideo(vurl, false, itag, Formats.get(itag)).setCodecMime(v.getString("mimeType"))
							.setFileSize(v.getLong("contentLength", -1)));
				}
			}
			logger.debug("=> Processing new map.");
			try {
				final UniJsonIterableValue aFormats = streamingData.get("adaptiveFormats").asArray();
				if (aFormats.isNull()) {
					logger.warn("Missing adaptiveFormats!");
					logger.warn(player_response.toString());
				} else {
					for (final UniJsonValue _v : aFormats) {
						final UniJsonValue v = _v;
						String vurl = v.getString("url", null);
						if (v.contains("cipher", "signatureCipher")) {
							@SuppressWarnings("deprecation")
							final Map<String, String> y = LegacyURIUtils
									.queryStringToMap(v.getStrings("signatureCipher", "cipher").replaceAll(Pattern.quote("\\u0026"), "&"));
							vurl = y.get("url");
							vurl += "&" + y.get("sp") + "=" + e.waitTillCompletion().decrypt(y.get("s"));
						}
						final int itag = v.getInt("itag");
						if (vurl.contains("&dur=0.000")) {
							logger.warn("A source had zero duration, skipped.");
							continue;
						}
						if (useProxying.get()) {
							vurl = VIDEO_PROXY + "/direct/" + UniBase64.encodeFromString(vurl);
						}
						results.add(new YouTubeVideo(vurl, true, itag, Formats.get(itag)).setCodecMime(v.getString("mimeType"))
								.setFileSize(v.getLong("contentLength", -1)).setSize(v.getInt("width", 0) + "x" + v.getInt("height", 0))
								.setInitPayload(v.get("initRange").getString("start") + "-" + v.get("initRange").getString("end"))
								.setIndexPayload(v.get("indexRange").getString("start") + "-" + v.get("indexRange").getString("end"))
								.setFPS(v.getInt("fps", -1)).setBitrate(v.getInt("bitrate", 80000)));
					}
				}
			} catch (Exception _e) {
				logger.error("Hit an error while processing an entry!", _e);
			}
			if (player_response.hasValue("streamingData")) {
				if (streamingData.hasValue("hlsManifestUrl")) {
					logger.debug("Parsing a live video...");
					final String hlsMetadata = streamingData.getString("hlsManifestUrl");
					results.clear();
					results.add(new YouTubeVideo(hlsMetadata, false, 0, new Format(0, FileType.M3U8, 30, 720, VideoCodec.H264, AudioCodec.AAC, -1, false))
							.setLivestream(true).setInternalProxy(true).setFormatType(FormatType.M3U8_STREAM));
					return new ExtractedVideo(results, metadata, ExtractionResult.SUCCESS);
				}
			}
			if (results.isEmpty()) {
				return new ExtractedVideo(results, metadata, ExtractionResult.FAILED_GEOGRAPHICALLY_BLOCKED).setMessage("ERROR_NO_VIDEOS_EXTRACTED")
						.setYTMessage("Failed to get any video source. Is this video geographically restricted?");
			}
			return new ExtractedVideo(results, metadata, ExtractionResult.SUCCESS).setPlaylist(pl);
		});
	}

	public static void main(String[] args) {
		final IYouTubeExtractor ex = YouTubeExtractor.get("YTE.third_party.simple_age_restriction_bypass");
		ex.enableInstanceDebugging(true);
		final Executable<ExtractedVideo> f = ex.generateTask(FastMaps.fixedUniMap(new Object[][]
			{
					{ IYouTubeExtractorConstants.MAP_TYPE_VIDEO_ID, "DiwSm6QgZM8" },
					{ IYouTubeExtractorConstants.MAP_TYPE_USE_EXPERIMENTAL_BACKEND_FLAG, true },
					{ IYouTubeExtractorConstants.MAP_TYPE_ENABLE_DEBUG_API_RESPONSE_PRINT, true } }));
		ExecutorManager.get().asyncSubmit(f);
		final long s0 = System.currentTimeMillis();
		JVM.println("result: " + f.waitTillCompletion());
		final long e0 = System.currentTimeMillis();
		JVM.println("Execution Time: " + (e0 - s0) + " ms");
		if (f.result() == null) {
			throw new NullPointerException("Failed to get a result!");
		}
		if (f.result().supportsLateExtraction()) {
			f.result().awaitCompletion();
			JVM.println("lateExtraction: COMPLETE");
		}
		if (f.result().hasCommentData()) {
			JVM.println("commentData: " + f.result().getCommentData());
		}
		final long s = System.currentTimeMillis();
		if (f.result().getMetadata() != null)
			JVM.println("enhanced description: " + f.result().getMetadata().getEnhancedDescription());
		final long e = System.currentTimeMillis();
		JVM.println("lat: " + (e - s) + " ms");
		if (f.result() != null) {
			final List<YouTubeVideo> result = f.waitTillCompletion().videos();
			ArrayUtils.printLln(result);
			JVM.println(f.result().getMetadata());
		}
		FastIO.get().flush();
		JVM.exit();
	}

	@Override
	public String getID() {
		return "YTE.third_party.simple_age_restriction_bypass";
	}

	@Override
	public int abiVersion() {
		return 2000;
	}

	@Override
	public Object getFlag(final int flag) {
		switch (flag) {
			case FLAG_EXTRACTOR_SUPPORTS_AGE_GATE:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAM_CHAT:
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAMS:
			case FLAG_EXTRACTOR_SUPPORTS_LATE_EXTRACTION:
			case FLAG_EXTRACTOR_SUPPORTS_SEGMENTED_EXTRACTION:
				return FLAG_FALSE;
			case FLAG_EXTRACTOR_SUPPORTS_ABI_2_2:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_GET_SUPPORTED_ABI:
				return 2.2f;
			default:
				return FLAG_NOT_SUPPORTED;
		}
	}

}
